using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;

namespace Actya.Web.Infrastructure.AntiForgery
{
	internal class AjaxAntiForgeryDataSerializer
	{

		private IStateFormatter _formatter;

		protected internal IStateFormatter Formatter
		{
			get
			{
				if (_formatter == null)
				{
					_formatter = FormatterGenerator.GetFormatter();
				}
				return _formatter;
			}
			set
			{
				_formatter = value;
			}
		}

		private static HttpAntiForgeryException CreateValidationException(Exception innerException)
		{
			return new HttpAntiForgeryException("Antiforgerytoken missing or validation failed.", innerException);
		}

		public virtual AjaxAntiForgeryData Deserialize(string serializedToken)
		{
			if (String.IsNullOrEmpty(serializedToken))
			{
				throw new ArgumentException("Value cannot be null or empty.", "serializedToken");
			}

			// call property getter outside try { } block so that exceptions bubble up for debugging
			IStateFormatter formatter = Formatter;

			try
			{
				object[] deserializedObj = (object[])formatter.Deserialize(serializedToken);
				return new AjaxAntiForgeryData()
				{
					Salt = (string)deserializedObj[0],
					Value = (string)deserializedObj[1],
					CreationDate = (DateTime)deserializedObj[2],
					Username = (string)deserializedObj[3]
				};
			}
			catch (Exception ex)
			{
				throw CreateValidationException(ex);
			}
		}

		public virtual string Serialize(AjaxAntiForgeryData token)
		{
			if (token == null)
			{
				throw new ArgumentNullException("token");
			}

			object[] objToSerialize = new object[] {
                token.Salt,
                token.Value,
                token.CreationDate,
                token.Username
            };

			string serializedValue = Formatter.Serialize(objToSerialize);
			return serializedValue;
		}

		// See http://www.yoda.arachsys.com/csharp/singleton.html (fifth version - fully lazy) for the singleton pattern
		// used here. We need to defer the call to TokenPersister.CreateFormatterGenerator() until we're actually
		// servicing a request, else HttpContext.Current might be invalid in TokenPersister.CreateFormatterGenerator().
		private static class FormatterGenerator
		{

			public static readonly Func<IStateFormatter> GetFormatter = TokenPersister.CreateFormatterGenerator();

			[SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline",
				Justification = "This type must not be marked 'beforefieldinit'.")]
			static FormatterGenerator()
			{
			}

			// This type is very difficult to unit-test because Page.ProcessRequest() requires mocking
			// much of the hosting environment. For now, we can perform functional tests of this feature.
			private sealed class TokenPersister : PageStatePersister
			{
				private TokenPersister(Page page)
					: base(page)
				{
				}

				public static Func<IStateFormatter> CreateFormatterGenerator()
				{
					// This code instantiates a page and tricks it into thinking that it's servicing
					// a postback scenario with encrypted ViewState, which is required to make the
					// StateFormatter properly decrypt data. Specifically, this code sets the
					// internal Page.ContainsEncryptedViewState flag.
					TextWriter writer = TextWriter.Null;
					HttpResponse response = new HttpResponse(writer);
					HttpRequest request = new HttpRequest("DummyFile.aspx", HttpContext.Current.Request.Url.ToString(), "__EVENTTARGET=true&__VIEWSTATEENCRYPTED=true");
					HttpContext context = new HttpContext(request, response);

					Page page = new Page()
					{
						EnableViewStateMac = true,
						ViewStateEncryptionMode = ViewStateEncryptionMode.Always
					};
					page.ProcessRequest(context);

					return () => new TokenPersister(page).StateFormatter;
				}

				public override void Load()
				{
					throw new NotImplementedException();
				}

				public override void Save()
				{
					throw new NotImplementedException();
				}
			}
		}

	}
}